# 浏览器中的页面循环系统

1.消息队列和事件循环

  • 使用单线程处理安排好的任务,使用单线程顺序处理

1.2.在线程运行过程中处理新任务

  • 循环机制
  • 事件

1.3.处理其他线程发送过来的任务

  • 引入消息队列
  • 步骤
  • 添加一个消息队列
  • IO 线程中产生的新任务添加进消息队列尾部
  • 渲染主线程会循环地从消息队列头部中读取任务,执行任务

1.4.处理其他进程发送过来的任务

  • 渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息
  • 接收到消息之后,会将这些消息组装成任务发送给渲染主线程
  • 后续的步骤就和前面讲解的“处理其他线程发送的任务”一样

1.5.消息队列中的任务类型

1.6.如何安全退出

  • 确定要退出当前页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志
  • 如果设置了,那么就直接中断当前的所有任务,退出线程

1.7.页面使用单线程的缺点

  • 第一个问题是如何处理高优先级的任务
  • 微任务
  • 把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列
  • 第二个是如何解决单个任务执行时长过久的问题
  • 回调功能

2.WebAPI-setTimeout

  • 浏览器怎么实现 setTimeout
  • 渲染进程会将该定时器的回调任务添加到延迟队列中
  • ProcessDelayTask 函数

2.2.使用 setTimeout 的一些注意事项

  1. 如果当前任务执行时间过久,会影响延迟到期定时器任务的执行
  2. 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
  3. 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
  4. 延时执行时间有最大值
  5. 使用 setTimeout 设置的回调函数中的 this 不符合直觉
  • 问题--如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象
  • 解决
  1. 第一种是将MyObj.showName放在匿名函数中执行
  2. 第二种是使用 bind 方法,将 showName 绑定在 MyObj 上面

3.WebAPI-XMLHttpRequest

3.1.准备知识

  • 回调函数
  • 将一个函数作为参数传递给另外一个函数,那作为参数的这个函数就是回调函数
  • 分类--
  1. 同步回调; 2.异步回调
  • 异步回调是指回调函数在主函数之外执行

  • 一般有两种方式

    • 第一种是把异步函数做成一个任务,添加到信息队列尾部
    • 第二种是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务了
  • 消息队列和主线程循环机制保证了页面有条不紊地运行

  • 系统调用栈

3.2.XMLHttpRequest 运作机制

  • 新建 XMLHttpRequest 请求对象
  • 注册相关事件回调处理函数
  • 打开请求
  • 配置参数
  • 发送请求

3.3.XMLHttpRequest 使用过程中的“坑”

  1. 跨域问题
  2. HTTPS 混合内容的问题
  3. 使用 XMLHttpRequest 混合资源失效

4.宏任务与微任务

4.1.宏任务

  • 在WHATWG 规范中是怎么定义事件循环机制的
  • 消息队列中宏任务的执行过程
  • 先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask
  • 然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任务
  • 当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask
  • 最后统计执行完成的时长等信息
  • 所以说宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如后面要介绍的监听 DOM 变化的需求

4.2.复习

  • 异步回调
  • 第一种是把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数
    • 比如 setTimeout 和 XMLHttpRequest
  • 第二种方式的执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,
    • 这通常都是以微任务形式体现的

4.3.微任务

  • 微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
  • 微任务队列
  • 每个宏任务都关联了一个微任务队列
  • 微任务产生的时机
  • 在现代浏览器里面,产生微任务有两种方式
    • 第一种方式是使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务
    • 第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务
  • 执行微任务队列的时机
  • WHATWG 把执行微任务的时间点称为检查点
    • 通常情况下,在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务
    • 当然除了在退出全局执行上下文式这个检查点之外,还有其他的检查点,不过不是太重要,这里就不做介绍了
  • 如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,V8 引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束
  • 也就是说在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行
  • 监听 DOM 变化方法演变
  • 轮询检测
  • Mutation Event
    • Mutation Event 采用了观察者的设计模式,当 DOM 有变动时就会立刻触发相应的事件,这种方式属于同步回调
  • MutationObserver
    • 将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用
    • 并且还会使用一个数据结构来记录这期间所有的 DOM 变化。这样即使频繁地操纵 DOM,也不会对性能造成太大的影响
    • 综上所述
      • MutationObserver 采用了“异步 + 微任务”的策略
      • 通过异步操作解决了同步操作的性能问题
      • 通过微任务解决了实时性的问题
13-1-浏览器中的页面循环系统.jpg

# 浏览器中的 Event Loop

event loop它的执行顺序:

  1. 一开始整个脚本作为一个宏任务执行
  2. 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  3. 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
  4. 执行浏览器UI线程的渲染工作
  5. 检查是否有Web Worker任务,有则执行
  6. 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
  • 微任务包括:MutationObserver、Promise.then()或catch()、Promise为基础开发的其它技术,比如fetch API、V8的垃圾回收过程、Node独有的process.nextTick。
  • 宏任务包括:script 、setTimeout、setInterval 、setImmediate 、I/O 、UI rendering。

注意

在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。

这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。

上次更新: 2021年10月30日星期六晚上8点03分